﻿namespace Custom.Windows
{
    #region Namespaces
    using System;
    using System.Diagnostics;
    using System.Linq;
    using System.Reflection;
    using System.Runtime.InteropServices;
    using System.Security;
    using System.Security.AccessControl;
    using System.Security.Permissions;
    using System.Security.Principal;
    using System.ServiceModel;
    using System.Threading;
    using System.Windows;
    using System.Windows.Threading;

    #endregion

    /// <summary>
    ///     Class used to define a WPF application which is aware of subsequent application instances running, either locally
    ///     (per session) or globally (per host).
    /// </summary>
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
    public class InstanceAwareApplication : Application, IPriorApplicationInstance, IDisposable
    {
        #region Constants
        /// <summary>
        ///     The milliseconds to wait to determine if the current instance is the first one.
        /// </summary>
        private const double FirstInstanceTimeout = 500;

        /// <summary>
        ///     The mutex prefix used if the application instance must be single per machine.
        /// </summary>
        private const string GlobalPrefix = @"Global\";

        /// <summary>
        ///     The mutex prefix used if the application instance must be single per user session.
        /// </summary>
        private const string LocalPrefix = @"Local\";

        /// <summary>
        ///     The milliseconds to wait for the prior instance to signal that the startup information have been received.
        /// </summary>
        private const double PriorInstanceSignaledTimeout = 2500;

        /// <summary>
        ///     The milliseconds to wait for the service to be ready.
        /// </summary>
        private const double ServiceReadyTimeout = 1000;

        /// <summary>
        ///     The SID value to be used to retrieve the <c>Users</c> group identity.
        /// </summary>
        private const string UsersSID = "S-1-5-32-545";
        #endregion

        #region Static Methods
        /// <summary>
        ///     Extracts some parameters from the specified <see cref="ApplicationInstanceAwareness" /> value.
        /// </summary>
        /// <param name="awareness">The <see cref="ApplicationInstanceAwareness" /> value to extract parameters from.</param>
        /// <param name="prefix">The synchronization object prefix.</param>
        /// <param name="identity">The identity used to handle the synchronization object.</param>
        /// <exception cref="UnexpectedInstanceAwareApplicationException">A proper identity could not be determined.</exception>
        private static void ExtractParameters(ApplicationInstanceAwareness awareness, out string prefix, out IdentityReference identity)
        {
            new SecurityPermission(SecurityPermissionFlag.ControlPrincipal).Assert();
            var currentIdentity = WindowsIdentity.GetCurrent();

            if (currentIdentity != null)
            {
                if (awareness == ApplicationInstanceAwareness.Host && currentIdentity.Groups != null)
                {
                    prefix = GlobalPrefix;
                    identity = currentIdentity.Groups.FirstOrDefault(reference => reference.Translate(typeof(SecurityIdentifier))
                                                                                           .Value.Equals(UsersSID));
                }
                else
                {
                    prefix = LocalPrefix;
                    identity = currentIdentity.User;
                }
                CodeAccessPermission.RevertAssert();

                if (identity == null)
                    throw new UnexpectedInstanceAwareApplicationException(Custom.Windows.Properties.Resources.CannotDetermineIdentity);
            }
            else
                throw new UnexpectedInstanceAwareApplicationException(Custom.Windows.Properties.Resources.CannotRetrieveCurrentIdentity);
        }

        /// <summary>
        ///     Gets the <see cref="Uri" /> of the pipe used for inter-process communication.
        /// </summary>
        /// <param name="applicationPath">The application unique path, used to define the <see cref="Uri" /> pipe.</param>
        /// <returns>The <see cref="Uri" /> of the pipe used for inter-process communication.</returns>
        private static Uri GetPipeUri(string applicationPath)
        {
            return new Uri(string.Format("net.pipe://localhost/{0}/", applicationPath));
        }
        #endregion

        #region Events
        /// <summary>
        ///     Occurs when the <see cref="System.Windows.Application.Run()" /> or
        ///     <see cref="System.Windows.Application.Run(Window)" /> method of the next <see cref="InstanceAwareApplication" />
        ///     having the same <see cref="Guid" /> is called.
        /// </summary>
        public event StartupNextInstanceEventHandler StartupNextInstance;
        #endregion

        #region Fields
        /// <summary>
        ///     The synchronization object owned by the first instance.
        /// </summary>
        private Mutex _firstInstanceMutex;

        /// <summary>
        ///     Flag used to determine if the synchronization objects (and the inter-process communication service) have been
        ///     disposed.
        /// </summary>
        private bool _initialized;

        /// <summary>
        ///     The flag used to identify an instance as the first one.
        /// </summary>
        private bool? _isFirstInstance;

        /// <summary>
        ///     The host used to communicate between multiple application instances.
        /// </summary>
        private ServiceHost _serviceHost;

        /// <summary>
        ///     The synchronization object used to synchronize the service creation or destruction.
        /// </summary>
        private Mutex _serviceInitializationMutex;

        /// <summary>
        ///     The synchronization object used to signal that the service is ready.
        /// </summary>
        private EventWaitHandle _serviceReadySemaphore;

        /// <summary>
        ///     The synchronization object used to signal a subsequent application instance that the first one received the
        ///     notification.
        /// </summary>
        private EventWaitHandle _signaledToFirstInstanceSemaphore;
        #endregion

        #region Properties
        /// <summary>
        ///     Gets the instance awareness of the application.
        /// </summary>
        /// <value>The instance awareness of the application.</value>
        public ApplicationInstanceAwareness Awareness { get; private set; }

        /// <summary>
        ///     Gets a value indicating whether the current application instance is the first one.
        /// </summary>
        /// <value>
        ///     <c>True</c> if the current application instance is the first one, otherwise <c>false</c>.
        /// </value>
        /// <remarks>
        ///     The first application instance gets notified about subsequent application instances startup.
        /// </remarks>
        public bool? IsFirstInstance
        {
            get
            {
                VerifyAccess();
                return _isFirstInstance;
            }
        }
        #endregion

        /// <summary>
        ///     Initializes a new instance of the <see cref="InstanceAwareApplication" /> class.
        /// </summary>
        /// <exception cref="System.InvalidOperationException">
        ///     More than one instance of the
        ///     <see cref="System.Windows.Application" /> class is created per <see cref="System.AppDomain" />.
        /// </exception>
        public InstanceAwareApplication()
                : this(ApplicationInstanceAwareness.Host)
        {
        }

        /// <summary>
        ///     Initializes a new instance of the <see cref="InstanceAwareApplication" /> class.
        /// </summary>
        /// <param name="awareness">The instance awareness of the application.</param>
        /// <exception cref="System.InvalidOperationException">
        ///     More than one instance of the
        ///     <see cref="System.Windows.Application" /> class is created per <see cref="System.AppDomain" />.
        /// </exception>
        public InstanceAwareApplication(ApplicationInstanceAwareness awareness)
        {
            Awareness = awareness;
        }

        #region IDisposable Members
        /// <summary>
        ///     Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        void IDisposable.Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        #endregion

        #region IPriorApplicationInstance Members
        /// <summary>
        ///     Signals the startup of the next application instance.
        /// </summary>
        /// <param name="args">The parameters used to run the next instance of the application.</param>
        void IPriorApplicationInstance.SignalStartupNextInstance(string[] args)
        {
            _signaledToFirstInstanceSemaphore.Set();

            ParameterizedThreadStart onStartupNextApplication = obj => OnStartupNextApplicationInstance((string[])obj);

            //Since the method is called asynchronously, invoke the function using the dispatcher!
            Dispatcher.BeginInvoke(onStartupNextApplication, DispatcherPriority.Background, (object)args);
        }
        #endregion

        /// <summary>
        ///     Gets the application unique identifier.
        /// </summary>
        /// <returns>The application unique identifier.</returns>
        private string GetApplicationId()
        {
            var id = GenerateApplicationId();
            if (!string.IsNullOrEmpty(id))
                return id;

            //If no identifier is defined, try to get the Assembly GUID!
            var assembly = Assembly.GetEntryAssembly();
            var guidAttribute = assembly.GetCustomAttributes(typeof(GuidAttribute), false)
                                        .FirstOrDefault(obj => (obj is GuidAttribute)) as GuidAttribute;
            if (guidAttribute != null)
                return guidAttribute.Value;
            throw new UndefinedApplicationGuidException(string.Format(Custom.Windows.Properties.Resources.NoGuidDefined_1, GetType()));
        }

        /// <summary>
        ///     Initializes the first application instance.
        /// </summary>
        /// <param name="uri">The <see cref="Uri" /> used by the service that allows for inter-process communication.</param>
        private void InitializeFirstInstance(Uri uri)
        {
            try
            {
                //Acquire the mutex used to synchornize service initialization...
                _serviceInitializationMutex.WaitOne();

                //Create and start the service...
                _serviceHost = new ServiceHost(this, uri);
                _serviceHost.AddServiceEndpoint(typeof(IPriorApplicationInstance), new NetNamedPipeBinding(), uri);
                _serviceHost.Open();

                //Release the mutex used to synchornize service initialization...
                _serviceInitializationMutex.ReleaseMutex();

                //Signal that the service is ready, so that subsequent instances can go on...
                _serviceReadySemaphore.Set();
            }
            catch (Exception exc)
            {
                throw new UnexpectedInstanceAwareApplicationException(Custom.Windows.Properties.Resources.FailedToCreateFirstInstanceService, exc);
            }
        }

        /// <summary>
        ///     Initializes the next application instance.
        /// </summary>
        /// <param name="uri">The <see cref="Uri" /> used by the service that allows for inter-process communication.</param>
        /// <param name="args">The arguments passed to the current instance.</param>
        /// <returns><c>True</c> if the prior instance was notified about curernt instance startup, otherwise <c>false</c>.</returns>
        private bool InitializeNextInstance(Uri uri, string[] args)
        {
            //Check if the service is up... wait a bit in case two applications are started simultaneously...
            if (!_serviceReadySemaphore.WaitOne(TimeSpan.FromMilliseconds(ServiceReadyTimeout), false))
                return false;

            try
            {
                var instance = ChannelFactory<IPriorApplicationInstance>.CreateChannel(new NetNamedPipeBinding(), new EndpointAddress(uri));
                instance.SignalStartupNextInstance(args);
            }
            catch (Exception exc)
            {
                Debug.WriteLine(string.Format("Exception while signaling first application instance (signal while first application shutdown?)\n{0}", exc),
                                GetType()
                                        .ToString());
                return false;
            }

            //If the first application does not notify back that the signal has been received, just return false...
            return _signaledToFirstInstanceSemaphore.WaitOne(TimeSpan.FromMilliseconds(PriorInstanceSignaledTimeout), false);
        }

        /// <summary>
        ///     Initializes the synchronization objects needed to deal with multiple instances of the same application.
        /// </summary>
        /// <param name="baseName">The base name of the synchronization objects.</param>
        /// <param name="identity">The identity to be associated to the synchronization objects.</param>
        private void InitializeSynchronizationObjects(string baseName, IdentityReference identity)
        {
            var firstInstanceMutexName = string.Format("{0}_FirstInstance", baseName);
            var serviceInitializationMutexName = string.Format("{0}_ServiceInitialization", baseName);
            var serviceReadySemaphoreName = string.Format("{0}_ServiceReady", baseName);
            var signaledToFirstInstanceSemaphoreName = string.Format("{0}_SignaledToFirstInstance", baseName);

            bool isNew;
            var eventRule = new EventWaitHandleAccessRule(identity, EventWaitHandleRights.FullControl, AccessControlType.Allow);
            var eventSecurity = new EventWaitHandleSecurity();
            eventSecurity.AddAccessRule(eventRule);

            var mutexRule = new MutexAccessRule(identity, MutexRights.FullControl, AccessControlType.Allow);
            var mutexSecurity = new MutexSecurity();
            mutexSecurity.AddAccessRule(mutexRule);

            _firstInstanceMutex = new Mutex(false, firstInstanceMutexName, out isNew, mutexSecurity);
            _serviceInitializationMutex = new Mutex(false, serviceInitializationMutexName, out isNew, mutexSecurity);
            _serviceReadySemaphore = new EventWaitHandle(false, EventResetMode.ManualReset, serviceReadySemaphoreName, out isNew, eventSecurity);
            _signaledToFirstInstanceSemaphore = new EventWaitHandle(false, EventResetMode.AutoReset, signaledToFirstInstanceSemaphoreName, out isNew, eventSecurity);
        }

        /// <summary>
        ///     Called on next application instance startup.
        /// </summary>
        /// <param name="args">The parameters used to run the next instance of the application.</param>
        private void OnStartupNextApplicationInstance(string[] args)
        {
            var e = new StartupNextInstanceEventArgs(args, true);
            OnStartupNextInstance(e);

            if (e.BringToForeground && (MainWindow != null))
            {
                (new UIPermission(UIPermissionWindow.AllWindows)).Assert();
                if (MainWindow.WindowState == WindowState.Minimized)
                    MainWindow.WindowState = WindowState.Normal;
                MainWindow.Activate();
                CodeAccessPermission.RevertAssert();
            }
        }

        /// <summary>
        ///     Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name="disposing">
        ///     <c>True</c> to release both managed and unmanaged resources, <c>false</c> to release only
        ///     unmanaged resources.
        /// </param>
        protected virtual void Dispose(bool disposing)
        {
            //Try to relinquish the synchronization objects, just in case the application did not exit...
            if (Dispatcher.Thread != Thread.CurrentThread)
                Dispatcher.BeginInvoke((Action)RelinquishSynchronizationObjects);
            else
                RelinquishSynchronizationObjects();
        }

        /// <summary>
        ///     Called when the the application identifier has to be generated.
        /// </summary>
        /// <returns>
        ///     The string used to identify the application univocally; or <see langword="null" /> or an empty string to
        ///     fallback to the <see cref="System.Runtime.InteropServices.GuidAttribute" /> value.
        /// </returns>
        /// <remarks>
        ///     <para>
        ///         The base implementation produces a <see langword="null" /> value; in such a case, if the entry assembly is
        ///         decorated with a <see cref="System.Runtime.InteropServices.GuidAttribute" />, that value will be used as an
        ///         application identifier.
        ///     </para>
        ///     <para>
        ///         Note that the method should return a <c>constant</c> value, since it is used
        ///         to mark univocally the application.
        ///     </para>
        ///     <para>
        ///         The encouraged approach to mark an application univocally, is marking the entry assembly with a proper
        ///         <see cref="System.Runtime.InteropServices.GuidAttribute" />; this method should be used only if such a
        ///         method
        ///         is impractical or not possible.
        ///     </para>
        /// </remarks>
        protected virtual string GenerateApplicationId()
        {
            return null;
        }

        /// <summary>
        ///     Initializes the synchronization objects.
        /// </summary>
        /// <returns>
        ///     <c>True</c> if the current instance is the first application instance; <c>false</c> if the instance is not the
        ///     first; <see langword="null" /> if it is not possible to determine the application state.
        /// </returns>
        protected bool? InitializeSynchronizationObjects()
        {
            if (!_initialized)
            {
                var id = GetApplicationId();

                string prefix;
                IdentityReference identity;

                //Extract synchronization objects parameters...
                ExtractParameters(Awareness, out prefix, out identity);

                //Initialize synchornization objects...
                InitializeSynchronizationObjects(string.Format("{0}{1}", prefix, id), identity);

                //The mutex is acquired by the first instance, and never released until first application shutdown!
                bool isFirstInstance;

                try
                {
                    isFirstInstance = _firstInstanceMutex.WaitOne(TimeSpan.FromMilliseconds(FirstInstanceTimeout));
                }
                catch (AbandonedMutexException)
                {
                    //Debug.WriteLine("The previous application was probably killed, we can just handle the exception since in this case is not fatal!");
                    isFirstInstance = true;
                }

                var uri = GetPipeUri(string.Format("{0}/{1}", id, identity));
                if (isFirstInstance)
                    InitializeFirstInstance(uri);
                else
                {
                    if (InitializeNextInstance(uri, Environment.GetCommandLineArgs()))
                        OnStartupSignaledToPriorApplicationSucceeded();
                    else
                        OnStartupSignaledToPriorApplicationFailed();
                }

                _initialized = true;
                return isFirstInstance;
            }

            return _isFirstInstance;
        }

        /// <summary>
        ///     Raises the <see cref="System.Windows.Application.Exit" /> event.
        /// </summary>
        /// <param name="e">An <see cref="System.Windows.ExitEventArgs" /> that contains the event data.</param>
        protected override sealed void OnExit(ExitEventArgs e)
        {
            try
            {
                OnExit(e, _isFirstInstance.GetValueOrDefault());
            }
            finally
            {
                //On exit, try to dispose everything related to the synchronization context and the inter-process communication service...
                RelinquishSynchronizationObjects();
            }
        }

        /// <summary>
        ///     Raises the <see cref="System.Windows.Application.Exit" /> event.
        /// </summary>
        /// <param name="e">An <see cref="System.Windows.ExitEventArgs" /> that contains the event data.</param>
        /// <param name="isFirstInstance">If set to <c>true</c>, the current application instance is the first one.</param>
        protected virtual void OnExit(ExitEventArgs e, bool isFirstInstance)
        {
            base.OnExit(e);
        }

        /// <summary>
        ///     Called when the <see cref="IsFirstInstance" /> property value has changed.
        /// </summary>
        /// <param name="oldValue">The old value.</param>
        /// <param name="newValue">The new value.</param>
        protected virtual void OnIsFirstInstanceChanged(bool? oldValue, bool? newValue)
        {
        }

        /// <summary>
        ///     Raises the <see cref="System.Windows.Application.Startup" /> event.
        /// </summary>
        /// <param name="e">A <see cref="System.Windows.StartupEventArgs" /> that contains the event data.</param>
        protected override sealed void OnStartup(StartupEventArgs e)
        {
            var isFirstInstance = InitializeSynchronizationObjects();
            var oldValue = _isFirstInstance;
            _isFirstInstance = isFirstInstance;
            OnIsFirstInstanceChanged(oldValue, isFirstInstance);
            OnStartup(e, _isFirstInstance);
        }

        /// <summary>
        ///     Raises the <see cref="System.Windows.Application.Startup" /> event.
        /// </summary>
        /// <param name="e">The <see cref="System.Windows.StartupEventArgs" /> instance containing the event data.</param>
        /// <param name="isFirstInstance">
        ///     <c>True</c> if the current instance is the first application instance; <c>false</c> if
        ///     another instance has been detected; otherwise, <see langword="null" />.
        /// </param>
        protected virtual void OnStartup(StartupEventArgs e, bool? isFirstInstance)
        {
            base.OnStartup(e);
        }

        /// <summary>
        ///     Raises the <see cref="Custom.Windows.InstanceAwareApplication.StartupNextInstance" /> event.
        /// </summary>
        /// <param name="e">The <see cref="Custom.Windows.StartupNextInstanceEventArgs" /> instance containing the event data.</param>
        protected virtual void OnStartupNextInstance(StartupNextInstanceEventArgs e)
        {
            var startupNextInstanceEvent = StartupNextInstance;
            if (startupNextInstanceEvent != null)
                startupNextInstanceEvent(this, e);
        }

        /// <summary>
        ///     Called when the startup of the current application was unsuccessfully signaled to the prior application instance.
        /// </summary>
        protected virtual void OnStartupSignaledToPriorApplicationFailed()
        {
        }

        /// <summary>
        ///     Called when the startup of the current application was successfully signaled to the prior application instance.
        /// </summary>
        protected virtual void OnStartupSignaledToPriorApplicationSucceeded()
        {
        }

        /// <summary>
        ///     Relinquishes synchronization objects, if needed.
        /// </summary>
        protected void RelinquishSynchronizationObjects()
        {
            if (_initialized)
            {
                _initialized = false;

                var isFirstInstance = _isFirstInstance;

                if (isFirstInstance.HasValue) //Check if the application has been started...
                {
                    if (isFirstInstance.Value)
                    {
                        //Signal other applications that the service is not ready anymore (it is, but since the application is going to shutdown, it is the same...)
                        _serviceReadySemaphore.Reset();

                        //Stop the service...
                        _serviceInitializationMutex.WaitOne();

                        try
                        {
                            if (_serviceHost.State == CommunicationState.Opened)
                                _serviceHost.Close(TimeSpan.Zero); //Shut down the service without waiting!
                        }
                        catch (Exception exc)
                        {
                            Debug.WriteLine(string.Format("Exception raised while closing service\n{0}", exc),
                                            GetType()
                                                    .ToString());
                        }
                        finally
                        {
                            _serviceHost = null;
                        }

                        try
                        {
                            _serviceInitializationMutex.ReleaseMutex();
                        }
                        catch (Exception exc)
                        {
                            Debug.WriteLine(string.Format("Unable to release service initialization mutex\n{0}", exc),
                                            GetType()
                                                    .Name);
                        }

                        try
                        {
                            //Release the first application mutex!
                            _firstInstanceMutex.ReleaseMutex();
                        }
                        catch (Exception exc)
                        {
                            Debug.WriteLine(string.Format("Unable to release first instance mutex\n{0}", exc),
                                            GetType()
                                                    .Name);
                        }
                    }

                    _firstInstanceMutex.Close();
                    _firstInstanceMutex = null;

                    _serviceInitializationMutex.Close();
                    _serviceInitializationMutex = null;

                    _serviceReadySemaphore.Close();
                    _serviceReadySemaphore = null;

                    _signaledToFirstInstanceSemaphore.Close();
                    _signaledToFirstInstanceSemaphore = null;
                }

                var oldValue = _isFirstInstance;
                _isFirstInstance = null;
                OnIsFirstInstanceChanged(oldValue, null);
            }
        }

        /// <summary>
        ///     Releases unmanaged resources and performs other cleanup operations before the
        ///     <see cref="InstanceAwareApplication" /> is reclaimed by garbage collection.
        /// </summary>
        ~InstanceAwareApplication()
        {
            Dispose(false);
        }
    }
}